﻿using System;

namespace MadLibrary.Text
{
    /// <summary>
    /// FlatFile class
    /// <para>A flat file database describes any of various means to encode a database model (most commonly a table) as a single file.</para>
    /// <para>Wiki: "http://en.wikipedia.org/wiki/Flat_file_database"</para>
    /// </summary>
    public class FlatFile
    {
        /// <summary>
        /// Sign position type
        /// </summary>
        public enum SignPosition
        {
            /// <summary>
            /// None
            /// </summary>
            None = 0,

            /// <summary>
            /// Leading sign
            /// </summary>
            LeadingSign = 1,

            /// <summary>
            /// Trailing sign
            /// </summary>
            TrailingSign = 2,
        }

        /// <summary>
        /// PadSide type
        /// </summary>
        public enum PadSide
        {
            /// <summary>
            /// Pad text from left
            /// </summary>
            Left = 0,

            /// <summary>
            /// Pad text from right
            /// </summary>
            Right = 1,
        }
    }

    /// <summary>
    /// FlatFile reader
    /// </summary>
    public class FlatFileReader : IFlatFileReader
    {
        private char[] _recordSize;
        private string _source;

        /// <summary>
        /// Record size
        /// </summary>
        public int RecordSize
        {
            get { return _recordSize.Length; }
        }

        /// <summary>
        /// Source string
        /// </summary>
        public string Source
        {
            get { return _source; }
        }

        /// <summary>
        /// Initialize source to read
        /// </summary>
        /// <param name="source"></param>
        public void Initialize(string source)
        {
            if (source == (string)null) throw new ArgumentNullException("source");

            _source = source;

            int maxPos = source.IndexOf('\0'); //find last readable character
            _recordSize = maxPos < 0 ? source.ToCharArray() : source.Substring(0, maxPos).ToCharArray();
        }

        /// <summary>
        /// Read boolean type
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length to read.</param>
        /// <returns>Boolean type value</returns>
        public bool ReadBool(int start, int length)
        {
            //function return value
            bool functionReturnValue = false;

            string strValue = ReadString(start, length);

            if (!string.IsNullOrEmpty(strValue) && strValue == "1") //1 - true, 0 - false
            {
                //set return
                functionReturnValue = true;
            }

            //return
            return functionReturnValue;
        }

        /// <summary>
        /// Read date
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <returns>nullable DateTime value</returns>
        public DateTime? ReadDate(int start, int length)
        {
            //function return value
            DateTime? functionReturnValue = null;

            string strValue = ReadString(start, length);
            if (strValue.Length == 10)
            {
                functionReturnValue = DateTime.Parse(strValue);
            }

            //return
            return functionReturnValue;
        }

        /// <summary>
        /// Read decimal
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <param name="decimalPlaces">Decimal places</param>
        /// <param name="signEnum">+/- sign</param>
        /// <returns>Decimal value</returns>
        public decimal ReadDecimal(int start, int length, int decimalPlaces, FlatFile.SignPosition signEnum)
        {
            //functon return value
            decimal functionReturnValue = 0;

            string strValue = ReadString(start, length);

            if (string.IsNullOrEmpty(strValue))
            {
                return functionReturnValue;
            }
            if (signEnum == FlatFile.SignPosition.TrailingSign)
            {
                strValue = strValue.Substring(length - 1, 1) + strValue.Substring(0, length - 1);
            }

            bool result = decimal.TryParse(strValue, out functionReturnValue);
            if (result && decimalPlaces > 0)
            {
                return functionReturnValue / (decimal)(System.Math.Pow(10, decimalPlaces));
            }
            else
            {
                return functionReturnValue;
            }
        }

        /// <summary>
        /// Read double
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <param name="decimalPlaces">Decimal places</param>
        /// <param name="signEnum">+/- sign</param>
        /// <returns>Double value</returns>
        public double ReadDouble(int start, int length, int decimalPlaces, FlatFile.SignPosition signEnum)
        {
            //functon return value
            double functionReturnValue = 0;

            string strValue = ReadString(start, length);

            if (string.IsNullOrEmpty(strValue))
            {
                return functionReturnValue;
            }
            if (signEnum == FlatFile.SignPosition.TrailingSign)
            {
                strValue = strValue.Substring(length - 1, 1) + strValue.Substring(0, length - 1);
            }

            bool result = double.TryParse(strValue, out functionReturnValue);

            if (result && decimalPlaces > 0)
            {
                return functionReturnValue / (System.Math.Pow(10, decimalPlaces));
            }
            else
            {
                return functionReturnValue;
            }
        }

        /// <summary>
        /// Read integer
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <returns>Int value</returns>
        public int ReadInteger(int start, int length)
        {
            //return
            return ReadInteger(start, length, FlatFile.SignPosition.None);
        }

        /// <summary>
        /// Read integer
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <param name="signEnum">+/- sign</param>
        /// <returns>Int value</returns>
        public int ReadInteger(int start, int length, FlatFile.SignPosition signEnum)
        {
            //function return value
            int functionReturnValue = 0;

            string strValue = ReadString(start, length);

            if (string.IsNullOrEmpty(strValue))
            {
                return functionReturnValue;
            }
            if (signEnum == FlatFile.SignPosition.TrailingSign)
            {
                strValue = strValue.Substring(length - 1, 1) + strValue.Substring(0, length - 1);
            }
            bool result = int.TryParse(strValue, out functionReturnValue);

            //return
            return functionReturnValue;
        }

        /// <summary>
        /// Read long
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <returns>Long value</returns>
        public long ReadLong(int start, int length)
        {
            //return
            return ReadLong(start, length, FlatFile.SignPosition.None);
        }

        /// <summary>
        /// Read long
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <param name="signEnum">+/- sign</param>
        /// <returns>Long value</returns>
        public long ReadLong(int start, int length, FlatFile.SignPosition signEnum)
        {
            //function return value
            long functionReturnValue = 0;

            string strValue = ReadString(start, length);

            if (string.IsNullOrEmpty(strValue))
            {
                return functionReturnValue;
            }
            if (signEnum == FlatFile.SignPosition.TrailingSign)
            {
                strValue = strValue.Substring(length - 1, 1) + strValue.Substring(0, length - 1);
            }
            bool result = long.TryParse(strValue, out functionReturnValue);

            //return
            return functionReturnValue;
        }

        /// <summary>
        /// Read short
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <returns>Short value</returns>
        public short ReadShort(int start, int length)
        {
            //return
            return ReadShort(start, length, FlatFile.SignPosition.None);
        }

        /// <summary>
        /// Read short
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <param name="signEnum"></param>
        /// <returns>Short value</returns>
        public short ReadShort(int start, int length, FlatFile.SignPosition signEnum)
        {
            //function return value
            short functionReturnValue = 0;

            string strValue = ReadString(start, length);

            if (string.IsNullOrEmpty(strValue))
            {
                return functionReturnValue;
            }

            if (signEnum == FlatFile.SignPosition.TrailingSign)
            {
                strValue = strValue.Substring(length - 1, 1) + strValue.Substring(0, length - 1);
            }

            bool result = short.TryParse(strValue, out functionReturnValue);

            //return
            return functionReturnValue;
        }

        /// <summary>
        /// Read string
        /// </summary>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length.</param>
        /// <returns>String value</returns>
        public string ReadString(int start, int length)
        {
            //functon return value
            string functionReturnValue = string.Empty;

            if (start >= 0 || length > 1)
            {
                char[] retValue = new char[length];

                for (int i = start; i < (start + length); i++)
                {
                    if (_recordSize.Length > i) retValue[i - start] = _recordSize[i];
                    else
                    {
                        return null;
                    }
                }

                //set return
                functionReturnValue = (new string(retValue)).Trim().Replace('\r', '\n'); //todo: don't know if we really need this replace
            }

            //return
            return functionReturnValue;
        }
    }

    /// <summary>
    /// FlatFile writer
    /// </summary>
    public class FlatFileWriter : IFlatFileWriter
    {
        private const char _defaultChar = ' ';
        private const char _defaultNumber = '0';
        private readonly char[] _record;
        private int _currentLenght = 0;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="recordSize">The length of the record to write.</param>
        public FlatFileWriter(int recordSize)
        {
            //char array
            _record = new char[recordSize];

            //fill string
            string fillString = new string(_defaultChar, recordSize);

            //set to char array
            _record = fillString.ToCharArray();
        }

        /// <summary>
        /// Lenght
        /// </summary>
        /// <returns>Current lenght of writen data</returns>
        public int Length()
        {
            return _currentLenght;
        }
        
        /// <summary>
        /// Get data
        /// </summary>
        /// <returns>Current writen data.</returns>
        public string GetData()
        {
            return new string(_record);
        }

        /// <summary>
        /// Write bool
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        public void WriteBool(bool? data, int start, int length)
        {
            if (!data.HasValue)
            {
                data = false;
            }
            WriteString(data == true ? "1" : "0", start, length);
        }

        /// <summary>
        /// Write datetime - nullable
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="dateFormat">Date format</param>
        public void WriteDate(DateTime? data, int start, int length, string dateFormat = "yyyy-MM-dd")
        {
            if (!data.HasValue)
            {
                WriteString(string.Empty, start, length);
            }
            else
            {
                WriteString(data.Value.ToString(dateFormat), start, length);
            }
        }

        /// <summary>
        /// Write decimal with sign position
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="decimalPlaces">Decimal places</param>
        /// <param name="signEnum">+/- sign</param>
        public void WriteDecimal(decimal? data, int start, int length, short decimalPlaces, FlatFile.SignPosition signEnum)
        {
            if (!data.HasValue)
            {
                data = 0;
            }

            //add minus sign if value negative
            bool addMinusSign = false;

            if (data < 0) addMinusSign = true;

            if (decimalPlaces > 0)
            {
                data = System.Math.Abs(data.Value) * (decimal)(System.Math.Pow(10, decimalPlaces));
            }

            string retValue = data.ToString();

            if (retValue.IndexOf(",") > -1)
            {
                // Truncate remaining decimal places
                retValue = retValue.Substring(0, retValue.IndexOf(","));
            }
            TransformNumeric(ref retValue, length, addMinusSign, signEnum);
            WriteString(retValue, start, length);
        }

        /// <summary>
        /// Write double with sign position
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="decimalPlaces">Decimal places</param>
        /// <param name="signEnum">+/- sign</param>
        public void WriteDouble(double? data, int start, int length, short decimalPlaces, FlatFile.SignPosition signEnum)
        {
            if (!data.HasValue)
            {
                data = 0;
            }

            //add minus sign if value negative
            bool addMinusSign = false;

            if (data < 0) addMinusSign = true;

            if (decimalPlaces > 0)
            {
                data = System.Math.Abs(data.Value) * (System.Math.Pow(10, decimalPlaces));
            }

            string retValue = data.ToString();

            if (retValue.IndexOf(".") > -1)
            {
                // Truncate remaining decimal places
                retValue = retValue.Substring(0, retValue.IndexOf("."));
            }
            TransformNumeric(ref retValue, length, addMinusSign, signEnum);
            WriteString(retValue, start, length);
        }

        /// <summary>
        /// Write integer
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        public void WriteInteger(int? data, int start, int length)
        {
            WriteInteger(data, start, length, FlatFile.SignPosition.None);
        }

        /// <summary>
        /// Write integer with sign position
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="signEnum">+/- sign</param>
        public void WriteInteger(int? data, int start, int length, FlatFile.SignPosition signEnum)
        {
            if (!data.HasValue)
            {
                data = 0;
            }
            string retValue = data.ToString();
            TransformNumeric(ref retValue, length, data < 0 ? true : false, signEnum);
            WriteString(retValue, start, length);

        }
        
        /// <summary>
        /// Write long
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        public void WriteLong(long? data, int start, int length)
        {
            WriteLong(data, start, length, FlatFile.SignPosition.None);
        }

        /// <summary>
        /// Write long with sign position
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="signEnum">+/- sign</param>
        public void WriteLong(long? data, int start, int length, FlatFile.SignPosition signEnum)
        {
            if (!data.HasValue)
            {
                data = 0;
            }
            string retValue = data.ToString();
            TransformNumeric(ref retValue, length, data < 0 ? true : false, signEnum);
            WriteString(retValue, start, length);
        }
        
        /// <summary>
        /// Write short
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        public void WriteShort(short? data, int start, int length)
        {
            WriteShort(data, start, length, FlatFile.SignPosition.None);
        }

        /// <summary>
        /// Write short with sign position
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="signEnum">+/- sign</param>
        public void WriteShort(short? data, int start, int length, FlatFile.SignPosition signEnum)
        {
            if (!data.HasValue)
            {
                data = 0;
            }
            string retValue = data.ToString();
            TransformNumeric(ref retValue, length, data < 0 ? true : false, signEnum);
            WriteString(retValue, start, length);
        }

        /// <summary>
        /// Write string
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        public void WriteString(string data, int start, int length)
        {
            if (data == null) data = string.Empty;

            //todo: don't know if we really need this replace
            data = data.Replace('\r', '\n'); //replacing cariage return with new line

            WriteString(data, start, length, FlatFile.PadSide.Right);
        }

        /// <summary>
        /// Write string
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        /// <param name="padSide">Pad side</param>
        public void WriteString(string data, int start, int length, FlatFile.PadSide padSide)
        {
            string padString = padSide == FlatFile.PadSide.Left ? data.PadLeft(length) : data.PadRight(length);
            WriteData(padString, start, length);
        }
        
        /// <summary>
        /// Write data
        /// </summary>
        /// <param name="data">Input data</param>
        /// <param name="start">Starting position in string. (zero based)</param>
        /// <param name="length">Length</param>
        private void WriteData(string data, int start, int length)
        {
            char[] charArray = data.ToCharArray();
            for (int i = start; i < (start + length); i++)
            {
                _record[i] = charArray[i - start];
            }
            _currentLenght += length;
        }

        /// <summary>
        /// Transform numeric by sign position
        /// </summary>
        /// <param name="retValue">Referenced value</param>
        /// <param name="length">Length</param>
        /// <param name="isNegative">Is negative</param>
        /// <param name="signEnum">+/- sign</param>
        private void TransformNumeric(ref string retValue, int length, bool isNegative, FlatFile.SignPosition signEnum)
        {
            switch (signEnum)
            {
                case FlatFile.SignPosition.None:
                    retValue = retValue.PadLeft(length, _defaultNumber);
                    break;
                case FlatFile.SignPosition.LeadingSign:
                    retValue = GetSign(isNegative) + retValue.PadLeft(length - 1, _defaultNumber);
                    break;
                case FlatFile.SignPosition.TrailingSign:
                    retValue = retValue.PadLeft(length - 1, _defaultNumber) + GetSign(isNegative);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("signEnum");
            }
        }

        /// <summary>
        /// Get sign
        /// </summary>
        /// <param name="isNegative">Is negative</param>
        /// <returns>Mathematical sign</returns>
        private static string GetSign(bool isNegative)
        {
            return isNegative ? "-" : "+";
        }
    }
}
